|
Contents
Modules vs. scriptsSCL source comes in two flavours that look deceptively similar but are compiled by two different front ends and obey different rules:
The SCL Console is a script that you type interactively. Everything this page says about scripts also holds for the console, with the two exceptions noted under Relative imports and Session lifetime. This page lists, exhaustively, what each side accepts and what it does not. The essential difference
A module is a definition of a vocabulary. A script is a transcript of a session. Almost every difference below follows from that one distinction. What may appear at the top levelThe script grammar accepts exactly two things at the top level: a statement and an import. Every other declaration form is module-only. When you hit this, the compiler tells you so by listing the tokens it was willing to accept:
Declaration forms
If you need any of these in a script, put them in a module and Statement formsA script's top level is a block, so it accepts syntactically the same statements as a
CHR is a module-only feature in practice: rules written in a script get as far as type checking and then fail in the code generator, because a CHR block compiles to a runtime class that belongs to the enclosing module. See 4.02 Constraint Handling Rules. In practice a script has exactly three usable statement forms: an expression, a value
definition and a function definition — plus Automatically imported modulesThis is the difference that surprises people most often, because it makes scripts look more capable than modules.
So this works in a script:
and the same line in a module fails with Couldn't resolve Set.fromList until you add
When you move code from a script into a module, expect to add imports, even for things that appeared to need none. Functions that exist only in a scriptThe command session installs a handful of functions that are not part of any module and that a module can therefore never call. They operate on the session itself.
While a script is being run with
They are restored to their previous values when that script finishes, so a nested script sees its own path, not its caller's. They do not exist when a script is run from the Model Browser, and they never exist in a module. Definitions in scriptsA definition in a module becomes a compiled constant of the module. A definition in a script becomes a session variable: an entry in a name-to-value map belonging to the command session. That has several consequences that module definitions do not share. Statements are compiled in groups, and the grouping is positionalThe session accumulates consecutive function definitions (a left-hand side with at
least one argument) into one compilation unit. Anything else — a value binding without
arguments, a bare expression, or an Within one group, a definition may refer forward to a later one:
Across groups it may not, because the earlier group is compiled as soon as the group ends. In this script
the value binding
In a module, the same three declarations compile without complaint, because a module has no notion of position. If a script needs helpers that call each other, keep the definitions adjacent — but note that if they call each other in a cycle, the compiler crashes rather than compiling them, see Known compiler defects. Pattern-matching clauses must be adjacentIn a module, all clauses of a function are collected by name no matter where they appear:
gives a two-clause In a script, the second
Adjacent clauses are fine, because they land in the same group:
Only simple bindings persistA statement becomes a session variable only if its left-hand side is a plain name or a function head. A destructuring pattern is accepted, executed, and then thrown away without a word:
The same is true of a Session variables are resolved lateA reference to a session variable compiles into a lookup by name, performed when the code runs, not when it is compiled. Redefining the variable therefore changes what already compiled functions see:
The type, however, was fixed when
Redefinition is legal in a script (including shadowing No type signatures, and no class-polymorphic definitionsA script cannot write a top-level
but it is not generalised over type class constraints. The constraint is resolved
when the definition itself is compiled, and the variable is stuck with the resulting
type — here
A module without a signature behaves the same way, but a module can add the signature and get the polymorphic definition it wants:
A script has no way to do this. Annotating the right-hand side does not help — a context in an inline annotation is rejected outright:
Annotating a concrete type does work, and is the usual workaround when you only need one instantiation:
If you need a genuinely class-polymorphic definition, it belongs in a module. Monadic bind does not work at the top levelA script statement group is closed off with a
Use Imports
|
| Relative imports | |
|---|---|
| Module | Yes, resolved against the module's name |
| SCL Script in the model | Yes, resolved against the script's URI |
| SCL Console | No — the console has no path to resolve against |
Script run with runFromFile |
No — the session's resolution name is unchanged |
Modules may not form an import cycle; the compiler reports Cyclic module dependency detected. A script cannot be imported at all, so it can never take part in a cycle.
A script has no place to put a module header, so none of the header's fields are available to it:
| Header field | Effect | Available to a script |
|---|---|---|
features = [fields] |
enables record.field access syntax |
No |
features = [edo] |
enables edo blocks |
No |
features = [chr] |
enables ruleset declarations and CHR select |
No |
export = [...] |
restricts what the module exports | not applicable |
defaultLocalName = "..." |
default prefix for importers | not applicable |
deprecated / deprecated = "..." |
marks the module deprecated | not applicable |
bundle = "..." |
picks the class loader for importJava |
not applicable |
The fields case is the one that bites. Given a record declared in a module that enables
the feature:
module {
features = [fields]
}
data Person = Person { name :: String, age :: Integer }
p = Person { name = "Ann", age = 30 }
pname r = r.name
a script can call pname p but cannot write p.name itself — without the fields
feature, . is not record field access but Simantics variable child browsing, which
needs the ReadGraph effect and does not see the record's fields. Write an accessor
function in the module and call that from the script.
Five identifiers are keywords or not depending on the module header, and the script front
end has no header to consult. Any occurrence of one of them anywhere in a script or
console command aborts the whole execution with a NullPointerException from the lexer:
edo rule ruleset select transformation
They cannot be used as variable names, and the constructs they introduce cannot be used
either. In particular select ... where { ... } queries cannot be written in a
script — wrap the query in a module function and call it.
A script statement is executed the moment it is compiled, in the order written. Effects happen exactly where you wrote them.
A module's top-level values are computed on first use and then memoised. Importing a
module runs nothing. Given a module Noisy containing
x = print "MODULE TOP LEVEL RAN"
a script sees this:
> import "Noisy"
> print "before"
before
> x
MODULE TOP LEVEL RAN
> x
The import printed nothing, the first reference to x performed the effect, and the
second reference printed nothing because the value had already been computed. Do not use
a module-level binding as a way to run something; put the effect in a function and call
it.
Both modules and scripts may define effectful values; neither restricts the effect.
| On an error | |
|---|---|
| Module | the module fails as a whole; nothing in it is usable |
| Script | statements before the failure have already run and their effects stand; execution stops at the first failing statement and the rest of the script is not run |
This applies to compilation errors and to exceptions alike. The script
print "one"
nosuchthing
print "three"
produces
> print "one"
one
> nosuchthing
Couldn't resolve nosuchthing.
and print "three" is never reached. Note that the error is not detected before the
script starts running: print "one" has already happened. A script is therefore not
atomic — a partially executed script can leave a half-configured model behind.
A script echoes each command it executes and prints the value of each statement using
show. A statement whose value is () prints nothing, and a value whose type has no
Show instance prints as a placeholder:
> take 0 []
<value of type [a]>
A module prints nothing at all when it is compiled or imported.
Session variables and the imports a script performed live in the command session, not in
the script. The SCL Console keeps one session for as long as the view is open, so
definitions accumulate there. Running an SCL Script from the Model Browser creates a
new command session for that run, so a script never inherits the console's variables,
and its own definitions do not leak back into the console. runFromFile, by contrast,
runs in the current session and does share it.
Use a module when the code defines things: types, classes, instances, reusable functions, Java imports, relations, operator precedences, documentation. Use a script when the code does things once: configuring a model, running a batch of edits, driving a sequence of operations.
The practical pattern is to keep everything of substance in modules and let the script be a short list of calls into them. That also sidesteps every limitation above.
| In the script | In the module |
|---|---|
implicit StandardLibrary |
add explicit imports for Set, String, File, Debug, ArrayList, ... |
| bare statements | wrap them in a function, e.g. main = do ... |
runFromFile, variables, reset, ... |
not available; drop them |
__SCRIPT_DIR__ |
pass the directory in as a parameter |
| adjacency-sensitive definitions | order no longer matters |
| monomorphic definitions | add :: signatures to make them polymorphic |
Mostly you cannot: data, type, class, instance, importJava, effect,
infixl, annotations and documentation strings have no script equivalent. Keep them in a
module and import it.
The following are defects rather than design decisions, and may be fixed in a later release. They are listed here because the failure modes are confusing — each one is a crash rather than a diagnostic. They are tracked as issue #1426.
| Trigger | Symptom |
|---|---|
| a cycle of two or more definitions that call each other | InternalCompilerError: ... variable <name> was not bounded |
the words edo, rule, ruleset, select, transformation |
NullPointerException from SCLLexer.supportEDO / supportCHR |
constraint ... at the top level |
UnsupportedOperationException from ConstraintStatement.mayBeRecursive |
The mutual recursion defect is worth spelling out, because it is wider than the script
top level. It affects every mutually recursive binding group evaluated outside a
module — at the script top level, in a let block, and in a where block:
> p x = q x
q x = if x == 0 then 0 else p (x-1)
InternalCompilerError: ... variable p was not bounded.
> let
p x = q x
q x = if x == 0 then 0 else p (x-1)
in p 3
InternalCompilerError: ... variable p was not bounded.
Self-recursion is unaffected; only cycles of length two or more fail. All three forms compile and run correctly inside a module, so the workaround is always the same: move the mutually recursive definitions into a module and call them from the script.
Because the SCL Console, SCL Scripts and embedded SCL expressions share the same evaluator, all three defects apply equally to all of them.